今天我們繼續來看 findRoute()
的實作。
/**
* Find the route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*/
protected function findRoute($request)
{
$this->events->dispatch(new Routing($request));
$this->current = $route = $this->routes->match($request);
$route->setContainer($this->container);
$this->container->instance(Route::class, $route);
return $route;
}
event dispatch 的部分我們先略過,往下看路由配對的部分。
這邊使用了 $this->routes->match
,也就是前面所定義的 RouteCollection
內的 match()
/**
* Find the first route matching a given request.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Routing\Route
*
* @throws \Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
// First, we will see if we can find a matching route for this current request
// method. If we can, great, we can just return it so that it can be called
// by the consumer. Otherwise we will check for routes with another verb.
$route = $this->matchAgainstRoutes($routes, $request);
return $this->handleMatchedRoute($request, $route);
}
這裡面 $routes = $this->get($request->getMethod());
裡面包含了一些預設 Http Method 和檢查的邏輯,不過我們先不細究。我們更關注的是 $this->matchAgainstRoutes()
這段的實作邏輯
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
return $routes->merge($fallbacks)->first(
fn (Route $route) => $route->matches($request, $includingMethod)
);
}
這一段看起來只是很純粹的 Laravel Collection 處理,不過仔細看過之後,我們可以發現到 $route->matches()
這一段才是實際上比對路由的實作項目。我們再往內追蹤看看 matches()
public function matches(Request $request, $includingMethod = true)
{
$this->compileRoute();
foreach (self::getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
裡面的 compileRoute()
則是
protected function compileRoute()
{
if (! $this->compiled) {
$this->compiled = $this->toSymfonyRoute()->compile();
}
return $this->compiled;
}
到這邊,我們終於經過層層解析,發現了 Laravel 是怎麼將使用者的請求對應到正確的路由:原來還是使用了 Symfony 的套件呀!
如果我們更深入的看 toSymfonyRoute()
可以看到 Laravel 所做的一些變化
public function toSymfonyRoute()
{
return new SymfonyRoute(
preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->uri()), $this->getOptionalParameterNames(),
$this->wheres, ['utf8' => true],
$this->getDomain() ?: '', [], $this->methods
);
}
雖然加上了這些變化,不過這其實不脫離 SymfonyRoute
原本設計的初衷,並且免去了 Laravel 開發時需要自己撰寫針對封包做字串處理的部分,像是 SymfonyRoute
需要實作的
private function extractInlineDefaultsAndRequirements(string $pattern): string
{
if (false === strpbrk($pattern, '?<')) {
return $pattern;
}
return preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) {
if (isset($m[4][0])) {
$this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null);
}
if (isset($m[3][0])) {
$this->setRequirement($m[2], substr($m[3], 1, -1));
}
return '{'.$m[1].$m[2].'}';
}, $pattern);
}
利用 Symfony 事先定義的處理,我們就可以省略這一段的解析,而是直接設計我們想要的路由結構了,是不是比想像中要簡單很多呢?